#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>
#include "md5.h"
#include "funsl.h"
#include <curl/curl.h>

#if (defined(_WIN32) || defined(__WIN32__)) && \
     !defined(WIN32) && !defined(__SYMBIAN32__)
#define WIN32
#endif

#ifdef WIN32
#include <winsock.h>
#else
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#endif


/*
 * TODO:
 * 1. Need synchronization in GetPacket, ReturnPacket, * SendOutgoingPacket, and
 *    GetIncomingPacket to be thread-safe
 * 2. Perhaps change to XmlRpc in Login?
 * 3. Better error handling everywhere
 * 4. Verify that PACKET_SIZE is big enough for the LL protocol
 */
#define NUM_PACKETS 10
#define PACKET_SIZE 1024

int num_free_packets=-1;
unsigned char zerobuffer[PACKET_SIZE];
unsigned char* free_udp_packets[NUM_PACKETS];
unsigned char udp_packets[NUM_PACKETS][PACKET_SIZE];

int udp_socket;
struct sockaddr_in server_addr;

struct MemoryStruct {
    char *memory;
    size_t size;
};

static void *myrealloc(void *ptr, size_t size)
{
    /* There might be a realloc() out there that doesn't like reallocing
       NULL pointers, so we take care of it here */
    if(ptr)
        return realloc(ptr, size);
    else
        return malloc(size);
}

void MD5(const char* plain_password, char* md5_password)
{
    MD5_CTX mdContext;
    MD5Init(&mdContext);
    MD5Update(&mdContext, plain_password, strlen(plain_password));
    MD5Final(&mdContext);
    /* Convert to hex string */
    sprintf(md5_password,
            "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
            mdContext.digest[0], mdContext.digest[1], mdContext.digest[2],
            mdContext.digest[3], mdContext.digest[4], mdContext.digest[5],
            mdContext.digest[6], mdContext.digest[7], mdContext.digest[8],
            mdContext.digest[9], mdContext.digest[10], mdContext.digest[11],
            mdContext.digest[12], mdContext.digest[13], mdContext.digest[14],
            mdContext.digest[15]);
}

static size_t HandleLoginReply(void *ptr, size_t size, size_t nmemb, void *data)
{
    size_t realsize = size * nmemb;
    struct MemoryStruct *mem = (struct MemoryStruct *)data;

    mem->memory = (char *)myrealloc(mem->memory, mem->size + realsize + 1);
    if (mem->memory) {
        memcpy(&(mem->memory[mem->size]), ptr, realsize);
        mem->size += realsize;
        mem->memory[mem->size] = 0;
    }
    return realsize;
}

void GetReplyData(char *text, char *pattern, char separator, char *output, int id)
{
    int i;
    int size;
    int offset;
    regex_t regex;
    regmatch_t result[2];

    *output='\0';
    regcomp(&regex, pattern, REG_EXTENDED);

    offset=0;
    for(i=0; i<id; i++) {
        if(regexec(&regex, &text[offset], 2, result, 0)==0) {
            size=strchr(&text[offset+result[1].rm_so], separator)-&text[offset+result[1].rm_so];
            memcpy(output, &text[offset+result[1].rm_so], size);
            output[size]='\0';
            offset=result[1].rm_so+size;
        }
    }
    regfree(&regex);
}

void Login(const char* url, const char* first_name, const char* last_name, const char* password, int* code, char* sessionID, char* agentID, char *region_url)
{
    struct MemoryStruct chunk;

    CURL *curl;
    struct curl_slist *headers=NULL;
    char login_msg[130000];
    char md5_password[100];
    int i, j, n;

#ifdef WIN32
WSADATA ws;
WSAStartup(0x0101,&ws);
#endif
    curl_global_init(CURL_GLOBAL_ALL);

    MD5(password, md5_password);
    sprintf(&login_msg[0], 
            "<llsd><map><key>mac</key><string>00:00:00:00:00:00</string><key>first</key><string>%s</string><key>id0</key><string>00:00:00:00:00:00</string><key>options</key><array><string>inventory-root</string><string>inventory-skeleton</string><string>inventory-lib-root</string><string>inventory-lib-owner</string><string>inventory-skel-lib</string><string>buddy-list</string></array><key>read_critical</key><boolean>1</boolean><key>last</key><string>%s</string><key>viewer_digest</key><string /><key>channel</key><string>My First Bot (libsecondlife)</string><key>version</key><string>Your name</string><key>platform</key><string>Linux</string><key>agree_to_tos</key><boolean>1</boolean><key>passwd</key><string>%s</string><key>start</key><string>last</string></map></llsd>",
            first_name, last_name, md5_password);

    sprintf(&login_msg[0], "<?xml version=\"1.0\"?><methodCall><methodName>login_to_simulator</methodName><params><param><value><struct><member><name>first</name><value><string>%s</string></value></member><member><name>last</name><value><string>%s</string></value></member><member><name>passwd</name><value><string>%s</string></value></member><member><name>start</name><value><string>uri:sim&amp;1&amp;2&amp;3</string></value></member><member><name>version</name><value><string>Second Life Release 1.19.1.4</string></value></member><member><name>channel</name><value><string>Second Life Release</string></value></member><member><name>platform</name><value><string>Win</string></value></member><member><name>mac</name><value><string>8c2538e4e50da2870cdd45da529e16c9</string></value></member><member><name>id0</name><value><string>2634c46794b6676262c8014dec8844fc</string></value></member><member><name>viewer_digest</name><value><string>bb2966f6-cf81-a0de-1e21-dcd21f05428a</string></value></member><member><name>last_exec_event</name><value><int>0</int></value></member><member><name>options</name><value><array><data><value><string>inventory-root</string></value><value><string>inventory-skeleton</string></value><value><string>inventory-lib-root</string></value><value><string>inventory-lib-owner</string></value><value><string>inventory-skel-lib</string></value><value><string>initial-outfit</string></value><value><string>gestures</string></value><value><string>event_categories</string></value><value><string>event_notifications</string></value><value><string>classified_categories</string></value><value><string>buddy-list</string></value><value><string>ui-config</string></value><value><string>login-flags</string></value><value><string>global-textures</string></value></data></array></value></member></struct></value></param></params></methodCall>",
            first_name, last_name, md5_password);

    chunk.memory=NULL;
    chunk.size = 0;

    curl = curl_easy_init();
    headers = curl_slist_append(headers, "Content-Type: text/xml");
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, &login_msg[0]);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(login_msg));
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HandleLoginReply);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) &chunk);
    curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    /* Extract important information from the reply */
    /* TODO: replace with proper XML parser */
    j=0; 
    strcpy(login_msg, chunk.memory);
    n=strlen(login_msg)+1;
    for(i=0; i<n; i++) {
        switch(login_msg[i]) {
            case '\n':
            case '\r':
            case ' ':
                break;
            default:
                chunk.memory[j++]=login_msg[i];
        }
    }

    GetReplyData(chunk.memory, "<name>circuit_code</name><value><i4>(.*?)</i4>", '<', sessionID, 1);
    *code=atoi(sessionID);
    GetReplyData(chunk.memory, "<name>session_id</name><value><string>(.*?)</string>", '<', sessionID, 1);
    GetReplyData(chunk.memory, "<name>agent_id</name><value><string>(.*?)</string>", '<', agentID, 2);
    GetReplyData(chunk.memory, "<name>sim_ip</name><value><string>(.*?)</string>", '<', login_msg, 1);
    GetReplyData(chunk.memory, "<name>sim_port</name><value><i4>(.*?)</i4>", '<', md5_password, 1);
    printf("CodeCircuit: %d\n", *code);
    printf("SessionID: %s\n", sessionID);
    printf("AgentID: %s\n", agentID);
    sprintf(region_url, "http://%s:%s", login_msg, md5_password);
    printf("RegionURL: %s\n", region_url);
    free(chunk.memory);
}

void Connect(const char* url)
{
    struct hostent *he;

    int port;
    char name[500];

    /* Parse the url */
    GetReplyData((char *) url, ".*:(.*?)$", '\0', name, 1);
    port=atoi(name);
    GetReplyData((char *) url, "http://(.*?):.*$", ':', name, 1);

    if ((he=gethostbyname(name)) == NULL) {
        perror("gethostbyname");
        exit(1);
    }

    if ((udp_socket = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr = *((struct in_addr *)he->h_addr);
    memset(server_addr.sin_zero, '\0', sizeof server_addr.sin_zero);
}

void Disconnect()
{
#ifdef WIN32
	// TODO: close(udp_socket) gives an assertion error in msvc2005. Why?
#else
    close(udp_socket);
#endif
    curl_global_cleanup();
}

/* Packet handling: UDP packets must be returned to avoid memory leaks and fragmentation */

void InitPacketPool()
{
    int i;
    for(i=0; i<NUM_PACKETS; i++) {
        free_udp_packets[i]=udp_packets[i];
    }
    num_free_packets=NUM_PACKETS-1;
}

unsigned char *GetPacket()
{
    if(num_free_packets==-1) InitPacketPool();
    if(num_free_packets==0) {
        printf("Out of packets error\n");
        exit(-1);
    }
    return free_udp_packets[num_free_packets--];
}

void ReturnPacket(unsigned char *packet)
{
    free_udp_packets[++num_free_packets]=packet;
}

int ZeroEncodePacket(unsigned char* packet, int length)
{
    int srclen=length;
    unsigned char *src=zerobuffer;
    unsigned char *dest=packet;

    int i;
    int bodylen;
    int zerolen = 0;
    unsigned char zerocount = 0;

    memcpy(zerobuffer, packet, length);

    memcpy(dest, src, 6);
    zerolen += 6;

    if (!HasAcks(src)) {
        bodylen = srclen;
    } else {
        bodylen = srclen - src[srclen - 1] * 4 - 1;
    }

    for (i = zerolen; i < bodylen; i++) {
        if (src[i] == 0x00) {
            zerocount++;

            if (zerocount == 0) {
                dest[zerolen++] = 0x00;
                dest[zerolen++] = 0xff;
                zerocount++;
            }
        } else {
            if (zerocount != 0) {
                dest[zerolen++] = 0x00;
                dest[zerolen++] = (unsigned char)zerocount;
                zerocount = 0;
            }
            dest[zerolen++] = src[i];
        }
    }

    if (zerocount != 0) {
        dest[zerolen++] = 0x00;
        dest[zerolen++] = (unsigned char)zerocount;
    }

    /* Copy appended ACKs */
    for (; i < srclen; i++) {
        dest[zerolen++] = src[i];
    }

    return zerolen;
}

int ZeroDecodePacket(unsigned char* packet, int length)
{
    int srclen=length;
    unsigned char *src=zerobuffer;
    unsigned char *dest=packet;

    int zerolen = 0;
    int bodylen = 0;
    int i = 0;
    unsigned char j;

    memcpy(zerobuffer, packet, length);

    memcpy(dest, src, 6);
    zerolen = 6;
    bodylen = srclen;

    for (i = zerolen; i < bodylen; i++) {
        if (src[i] == 0x00) {
            for (j = 0; j < src[i + 1]; j++) {
                dest[zerolen++] = 0x00;
            }

            i++;
        } else {
            dest[zerolen++] = src[i];
        }
    }

    /* Copy appended ACKs */
    for (; i < srclen; i++) {
        dest[zerolen++] = src[i];
    }

    return zerolen;
}

void SendOutgoingPacket(unsigned char* packet, int length)
{
    int n;
    if(IsZeroCoded(packet)) length=ZeroEncodePacket(packet, length);

    if ((n = sendto(udp_socket, packet, length, 0,
            (struct sockaddr *)&server_addr, sizeof server_addr)) == -1) {
        perror("sendto");
        exit(1);
    }
}

void GetIncomingPacket(unsigned char* packet, int* size, int timeout)
{
    socklen_t addr_len = sizeof(server_addr);

	// Wait for a packet
	while ((*size = recvfrom(udp_socket, packet, 1023 , 0, (struct sockaddr *)&server_addr, &addr_len)) == -1) {}

    if(IsZeroCoded(packet)) *size=ZeroDecodePacket(packet, *size);
}
